{- This file is part of funbot.
 -
 - Written in 2016, 2017 by fr33domlover <fr33domlover@riseup.net>.
 -
 - ♡ Copying is an act of love. Please copy, reuse and share.
 -
 - The author(s) have dedicated all copyright and related and neighboring
 - rights to this software to the public domain worldwide. This software is
 - distributed without any warranty.
 -
 - You should have received a copy of the CC0 Public Domain Dedication along
 - with this software. If not, see
 - <http://creativecommons.org/publicdomain/zero/1.0/>.
 -}

{-# LANGUAGE TypeFamilies               #-}

module Database.Persist.Schema
    ( FieldName (..)
    , EntityName (..)
    , UniqueName (..)
    , FieldType (..)
    , MaybeNull (..)
    , Field (..)
    , Entity (..)
    , Unique (..)
    , SchemaT
    , PersistSchema (..)
    )
where

import Control.Monad.IO.Class (MonadIO)
import Control.Monad.Trans.Reader (ReaderT)
import Data.Char (isAsciiLower, isAsciiUpper)
import Data.String (IsString (..))
import Data.Text (Text)
import Database.Persist.Types (SqlType)

import qualified Data.Text as T (pack, uncons, all, stripPrefix)

import Data.Char.Local (isAsciiLetter)

newtype FieldName = FieldName { unFieldName :: Text }

instance IsString FieldName where
    fromString s =
        let t = fromString s
        in  case T.uncons t of
                Nothing     -> error "empty field name"
                Just (c, r) ->
                    if isAsciiLower c
                        then
                            if T.all isAsciiLetter r
                                then FieldName t
                                else
                                    error "non ascii-letter char in field name"
                        else
                            error
                                "field name doesn't start with lowercase \
                                \ascii letter"

newtype EntityName = EntityName { unEntityName :: Text }

instance IsString EntityName where
    fromString s =
        let t = fromString s
        in  case T.uncons t of
                Nothing     -> error "empty entity name"
                Just (c, r) ->
                    if isAsciiUpper c
                        then
                            if T.all isAsciiLetter r
                                then EntityName t
                                else
                                    error
                                        "non ascii-letter char in entity name"
                        else
                            error
                                "entity name doesn't start with uppercase \
                                \ascii letter"

newtype UniqueName = UniqueName { unUniqueName :: Text }

instance IsString UniqueName where
    fromString s =
        let t = fromString s
        in  case T.stripPrefix (T.pack "Unique") t of
                Nothing -> error "unique name doesn't start with \"Unique\""
                Just u  ->
                    case T.uncons u of
                        Nothing     -> error "unique name is just \"Unique\""
                        Just (c, r) ->
                            if isAsciiUpper c
                                then
                                    if T.all isAsciiLetter r
                                        then UniqueName t
                                        else
                                            error
                                                "non ascii-letter char in \
                                                \unique name"
                                else
                                    error
                                        "unique name doesn't follow with \
                                        \uppercase ascii letter after Unique"

data FieldType = FTPrim SqlType | FTRef

data MaybeNull = MaybeNull | NotNull

data Field = Field
    { fieldName :: FieldName
    , fieldType :: FieldType
    , fieldNull :: MaybeNull
    }

data Entity = Entity
    { entityName    :: EntityName
    , entityFields  :: [Field]
    , entityUniques :: [Unique]
    }

data Unique = Unique
    { uniqueName   :: UniqueName
    , uniqueFields :: [FieldName]
    }

type SchemaT b m = ReaderT (SchemaBackend b) (ReaderT b m)

-- | Ideally we'd make the @backend@ provide schema related specifics. The
-- problem is that e.g. @SqlBackend@ is already defined in @persistent@ and
-- I'll need a patch to get it updated. A patch that will take time to get
-- accpted, if the maintainer likes it at all. So instead, I'm letting these
-- specifics be specified in a separate, associated data type.
--
-- The only benefit I see for this approach is schema changes are separate from
-- data manipulations. You can't mix them in a single transaction without
-- explicitly specifying the schema backend and using 'lift' for data manip.
class PersistSchema backend where
    data SchemaBackend backend -- :: *
    hasSchemaEntity
        :: MonadIO m => SchemaT backend m Bool
    addEntity
        :: MonadIO m => Entity -> SchemaT backend m ()
    removeEntity
        :: MonadIO m => EntityName -> SchemaT backend m ()
    addField
        :: MonadIO m
        => EntityName -> Maybe Text -> Field -> SchemaT backend m ()
    renameField
        :: MonadIO m
        => EntityName -> FieldName -> FieldName -> SchemaT backend m ()
    removeField
        :: MonadIO m => EntityName -> FieldName -> SchemaT backend m ()
    addUnique
        :: MonadIO m => EntityName -> Unique -> SchemaT backend m ()
    renameUnique
        :: MonadIO m
        => EntityName -> UniqueName -> UniqueName -> SchemaT backend m ()
    removeUnique
        :: MonadIO m => EntityName -> UniqueName -> SchemaT backend m ()
